﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Interpreter
{
	[System.Diagnostics.DebuggerDisplay("{DebugDisplay}")]
	public class Value
	{
		public string DebugDisplay
		{
			get
			{
				return this.ToString(Metadata._Instance);
			}
		}

		public const int TYPE_NULL = 1;
		public const int TYPE_BOOLEAN = 2;
		public const int TYPE_INTEGER = 3;
		public const int TYPE_FLOAT = 4;
		public const int TYPE_STRING = 5;
		public const int TYPE_LIST = 6;
		public const int TYPE_MAP = 7;
		public const int TYPE_FUNCTION = 8;
		public const int TYPE_STEP = 9;
		public const int TYPE_CLASS = 10;
		public const int TYPE_INSTANCE = 11;
		public const int TYPE_NAMESPACE = 12;
		public const int TYPE_EXCEPTION = 13;
		public const int TYPE_VARIABLE_POINTER = 14;
		public const int TYPE_BRACKET_POINTER = 15;
		public const int TYPE_DOT_POINTER = 16;
		public const int TYPE_NATIVE_OBJECT = 17;

		public const int TYPE_MAX = 18;

		public static readonly Value Null = new Value(TYPE_NULL, null);
		public static readonly Value True  = new Value(TYPE_BOOLEAN, true);
		public static readonly Value False = new Value(TYPE_BOOLEAN, false);
		public static readonly Value EMPTY_STRING = new Value(TYPE_STRING, "");

		public static readonly Value[] INTEGERS = new Value[1000];

		static Value()
		{
			for (int i = 0; i < 1000; ++i)
			{
				INTEGERS[i] = new Value(TYPE_INTEGER, i);
			}
		}

		public int ValueHash()
		{
			switch (this.Type)
			{
				case TYPE_NULL: return 0;
				case TYPE_BOOLEAN: return 1;
				case TYPE_INTEGER:
					return ((int)this.Value0 & 0x7fffffff);
				case TYPE_FLOAT: 
					int floatHash = (int)((double)this.Value0 % 16777216); // meh, isn't effective if it's a bunch of values between 0 and 1. But why are you keying off a float in the first place?
					return floatHash & 0x7fffffff;
				case TYPE_STRING:
					return this.Value0.GetHashCode() & 0x7fffffff; // TODO: determine if the hash of a string should be identical across platforms
				case TYPE_LIST:
				case TYPE_MAP:
					throw new NotImplementedException("hash values should not change based on contents, so must rethink == strategy for lists and maps.");
				case TYPE_CLASS:
					return (int)this.Value0;
				case TYPE_INSTANCE:
					// TODO: notion of a pointer? worried about exceeding int32.maxValue for long running programs?
					// if it's a hash, it doesn't have to be unique, 
					// Is a long good enough? should be.
					// For JavaScript, the pointer can be a tuple of ints.
					throw new NotImplementedException("hashing of instances.");
				default:
					throw new Exception("This should not happen.");
			}
		}

		public static Value NewClassDefinition(int classId)
		{
			// last value is initialization status
			// 0 - not initialized
			// 1 - currently being initialized
			// 2 - fully initialized
			// If an attempt is made to initialize a class while this value is 1, that means there's a static initialization dependency loop and an exception should be thrown.
			// There is no need to walk up the base class or nested class chain for initialization at runtime. A class reference for these are inserted
			// at the top of the static intializer function at compile-time, so this is done automatically.
			return new Value(Value.TYPE_CLASS, classId, new Dictionary<int, Value>(), 0);
		}

		public static Value NewClassInstance(int classId)
		{
			return new Value(Value.TYPE_INSTANCE, classId, new Dictionary<int, Value>());
		}

		public static Value GetInteger(int value)
		{
			if (value >= 0 && value < 1000)
			{
				return INTEGERS[value];
			}

			return new Value(TYPE_INTEGER, value);
		}

		public static Value GetString(string value)
		{
			return new Value(TYPE_STRING, value);
		}

		public int Type;

		public object Value0;
		public object Value1;
		public object Value2;

		public Value(int type, object value0)
		{
			this.Type = type;
			this.Value0 = value0;
		}

		public Value(int type, object value0, object value1)
		{
			this.Type = type;
			this.Value0 = value0;
			this.Value1 = value1;
		}

		public Value(int type, object value0, object value1, object value2)
		{
			this.Type = type;
			this.Value0 = value0;
			this.Value1 = value1;
			this.Value2 = value2;
		}

		public string ForceString(Metadata metadata)
		{
			return this.ForceString(new HashSet<object>(), metadata);
		}

		public string GetTypeAsString()
		{
			switch (this.Type)
			{
				case TYPE_NULL: return "null";
				case TYPE_BOOLEAN: return "Boolean";
				case TYPE_INTEGER: return "Integer";
				case TYPE_FLOAT: return "Float";
				case TYPE_STRING: return "String";
				case TYPE_FUNCTION: return "Function Pointer";
				case TYPE_LIST: return "List";
				case TYPE_MAP: return "Map";
				case TYPE_DOT_POINTER: return "Field";
				default:
					throw new NotImplementedException();
			}
		}

		internal string ForceString(HashSet<object> duplicateCheck, Metadata metadata)
		{
			switch (this.Type)
			{
				case TYPE_NULL: return "null";
				case TYPE_BOOLEAN: return ((bool)this.Value0) ? "true" : "false";
				case TYPE_LIST:
				case TYPE_MAP:
					bool isList = this.Type == TYPE_LIST;
					if (duplicateCheck.Contains(this.Value0))
					{
						return isList ? "[...]" : "{...}";
					}
					duplicateCheck.Add(this.Value0);

					// TOOD: fix bug -- algorithm needs to be tweaked a bit
					// list1 = [0, 0, 0];
					// list2 = [1, list1, 1];
					// list1[0] = list2;
					// list1[1] = list2;
					// list1[2] = list2;
					// print(list1);
					// Expected: [[1, [...], 1], [1, [...], 1], [1, [...], 1]]
					// Actual: [[1, [...], 1], [...], [...]]

					string output = isList
						? this.ForceListIntoString(duplicateCheck, metadata)
						: this.ForceMapIntoString(duplicateCheck, metadata);

					duplicateCheck.Remove(this.Value0);

					return output;

				case TYPE_INTEGER:
				case TYPE_FLOAT:
				case TYPE_STRING:
					return this.Value0.ToString();

				case TYPE_INSTANCE:
					return "Instance: <" + metadata.IdTable[metadata.ClassesFast[(int)this.Value0][0]] + ">"; // TODO: namespace

				case TYPE_NATIVE_OBJECT:
					// TODO: implement builtin-only valid field chars so that there is no way this can be invoked.
					return "{Native Object}";

				default:
					throw new NotImplementedException();
			}
		}

		private string ForceListIntoString(HashSet<object> duplicateCheck, Metadata metadata)
		{
			StringBuilder sb = new StringBuilder();
			Value[] list = (Value[])this.Value0;
			int listLength = (int)this.Value1;
			sb.Append("[");
			for (int i = 0; i < listLength; ++i)
			{
				if (i > 0)
				{
					sb.Append(", ");
				}
				sb.Append(list[i].ForceString(duplicateCheck, metadata));
			}
			sb.Append("]");

			return sb.ToString();
		}

		private string ForceMapIntoString(HashSet<object> duplicateCheck, Metadata metadata)
		{
			throw new NotImplementedException();
		}

		public string ToString(Metadata metadata)
		{
			string[] idTable = metadata.IdTable;
			switch (this.Type)
			{
				case Value.TYPE_NULL: return "<Null>";
				case Value.TYPE_BOOLEAN: return "<Bool> " + (this == Value.True ? "true" : "false");
				case Value.TYPE_INTEGER: return "<Int> " + this.Value0.ToString();
				case Value.TYPE_FLOAT: return "<Float> " + this.Value0.ToString();
				case Value.TYPE_CLASS: return "<Class> " + ClassUtil.GetName(metadata, (int)this.Value0);
				case Value.TYPE_INSTANCE: return "<Instance> " + ClassUtil.GetName(metadata, (int)this.Value0);
				case Value.TYPE_STRING: return "<String> \"" + this.Value0.ToString() + "\"";

				case Value.TYPE_LIST:
					Value[] list = (Value[])this.Value0;
					int listCount = (int)this.Value1;
					string output = "<List> (" + listCount + " items) ";

					if (listCount < 5)
					{
						output += "[";

						for (int i = 0; i < listCount; ++i)
						{
							if (i > 0)
							{
								output += ", ";
							}

							output += list[i].ToString(metadata);
						}

						output += "]";
					}
					return output;

				case Value.TYPE_FUNCTION:
					MethodSignature method = (MethodSignature)this.Value0;
					string signature = ClassUtil.GetName(metadata, method.ClassId) + "." + idTable[method.NameId] + "()"; ;
					return ((this.Value1 == null) ? "<Static Method> " : "<Method> ") + signature;

				case Value.TYPE_VARIABLE_POINTER:
					int variableId = (int)this.Value0;
					return "<Var Pointer> " + metadata.IdTable[variableId];

				case Value.TYPE_DOT_POINTER:
					bool isStatic = ((Value)this.Value0).Type == Value.TYPE_CLASS;
					return "<" + (isStatic ? "Static " : "") + "Member Pointer> " + ClassUtil.GetName(metadata, (int)((Value)this.Value0).Value0) + " -> " + idTable[(int)this.Value1];

				case Value.TYPE_NATIVE_OBJECT:
					List<string> pieces = new List<string>();
					if (this.Value0 != null)
					{
						pieces.Add("0: [" + this.Value0.ToString() + "]");
					}
					if (this.Value1 != null)
					{
						pieces.Add("1: [" + this.Value1.ToString() + "]");
					}
					if (this.Value2 != null)
					{
						pieces.Add("2: [" + this.Value2.ToString() + "]");
					}
					return "<Native Object> [" + string.Join(", ", pieces);

				default:
					return base.ToString();
			}
		}
	}
}
